package com.yhb.yinsheng;

import com.yhb.conf.ResultCode;
import com.yhb.controller.vo.SimpleVerifyCodeTokenVo;
import com.yhb.dao.RapidCheatMapper;
import com.yhb.dao.ThirdPurchaseFlowMapper;
import com.yhb.model.*;
import com.yhb.model.config.geo.GeoInfo;
import com.yhb.service.*;
import com.yhb.utils.Constants;
import com.yhb.dao.YinshengMapper;
import com.yhb.utils.DateUtils;
import com.yhb.utils.StringUtils;
import com.yhb.yinsheng.flow.UnionPayExecutionFlowMapper;
import com.yhb.yinsheng.model.*;
import lombok.extern.log4j.Log4j;
import lombok.var;
import me.edzh.spring.bootstrap.model.Tuple;
import me.edzh.spring.bootstrap.service.ObjectiveStorageService;
import me.edzh.spring.bootstrap.utils.JSONUtil;
import me.edzh.spring.bootstrap.utils.MathUtils;
import me.edzh.spring.bootstrap.utils.exception.ServiceException;
import me.edzh.spring.bootstrap.utils.http.ClientHolder;
import me.edzh.spring.bootstrap.utils.security.HashUtils;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Service
@Log4j
public class UnionPayService {

    @Value("${yinsheng.unionpay.baseUrl}")
    private String apiBaseUrl;

    @Value("${yinsheng.unionpay.accessCode}")
    private String accessCode;

    @Value("${yinsheng.unionpay.appKey}")
    private String appKey;

    private final static String PREFIX = "com.unionpay.yinsheng-";
    public final static String PREFIX_QR = "com.unionpay.yinsheng.qrcode-";

    private BigDecimal REMIT_FEE = BigDecimal.ZERO;

    @Autowired
    private CardService cardService;

    @Autowired
    private UserService userService;

    @Autowired
    private YinshengMapper yinshengMapper;

    @Autowired
    private RedisTemplate<String, String> redis;

    @Autowired
    private ObjectiveStorageService ossServiceImpl;

    @Autowired
    private ConfigFileService configFileService;

    @Autowired
    private RapidCheatMapper rapidCheatMapper;

    @Autowired
    private UnionPayExecutionFlowMapper unionPayExecutionFlowMapper;

    @Autowired
    private MessageService messageService;

    @Autowired
    private ThirdPurchaseFlowMapper thirdPurchaseFlowMapper;

    @Autowired
    @Lazy
    private TradeService tradeService;

    public ExecutionFlow getFlow(String flowId, String dateString) {
        return unionPayExecutionFlowMapper.getExecutionFlowById(flowId, dateString);
    }

    public QRCode genQRCode(User user, String payCardId) {
        Certificate cert = userService.getCertificate(user.getId());
        if (cert == null) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "请先实名认证");
        }
        Card remitCard = cardService.cardWithOwnerCheck(payCardId, user.getId());

        String userAppCode = HashUtils.getMD5(user.getId(), 20);

        var resp = request("/user/code", GenQRForm.builder()
                .appUserCode(userAppCode)
                .cardNo(remitCard.getNumber())
                .deviceId(Constants.getCurrentDeviceIdOrError())
                .gps(Constants.getCurrentGPSOrError())
                .deviceType(Constants.getCurrentDeviceTypeOrError())
                .ip(Constants.getCurrentRemoteIp())
                .build(), GenQRResp.class);

        log.info(JSONUtil.toJson(resp));
        int flushTime = Integer.parseInt(resp.getFlushTime());
        var code = QRCode.builder()
                .qrCode(resp.getQrCode())
                .validThrough(System.currentTimeMillis() + flushTime * 1000)
                .consumeCardId(payCardId)
                .userId(user.getId())
                .build();
        redis.boundValueOps(PREFIX_QR + resp.getQrCode()).set(JSONUtil.toJson(code), flushTime, TimeUnit.SECONDS);

        code.setConsumeCardId(null);
        code.setUserId(null);
        return code;
    }

    public String reverseScanQR(String province, String city, String qrCode, BigDecimal amount, User user, String remitCardId) {
        var codeStr = redis.boundValueOps(PREFIX_QR + qrCode).get();
        if (codeStr == null) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "二维码已过期");
        }

        redis.delete(PREFIX_QR + qrCode);
        var codeModel = JSONUtil.parseJson(codeStr, QRCode.class);

        var remitCard = cardService.cardWithOwnerCheck(remitCardId, user.getId());
        if (remitCard.getType() != Card.Type.DEBIT) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "收款卡必须是储蓄卡");
        }
        var consumeCard = cardService.cardWithOwnerCheck(codeModel.getConsumeCardId(), user.getId());

        if (consumeCard.getNumber().equals(remitCard.getNumber())) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "禁止同卡进出");
        }
        var flow = pay(province, city, codeModel, amount, user);

        var cheat = RapidCheat.builder()
                .type(RapidCheat.Type.UNION_PAY_CHANNEL)
                .userId(user.getId())
                .amount(amount)
                .consumeCardId(codeModel.getConsumeCardId())
                .remitCardId(remitCardId)
                .status(RapidCheat.Status.CREATED)
                .consumeFlowId(flow.getId())
                .commission(flow.getCommission())
                .remitFee(REMIT_FEE)
                .consumeCardBank(consumeCard.getBankName())
                .consumeCardType(consumeCard.getType())
                .consumeCardNumber(consumeCard.getNumber())
                .remitCardBank(remitCard.getBankName())
                .remitCardType(remitCard.getType())
                .remitCardNumber(remitCard.getNumber())
                .createTime(new Date())
                .build();

        rapidCheatMapper.insert(cheat);

        if (flow.getStatus() == ExecutionFlow.Status.FAILED) {
            onConsumeFailed(cheat, flow, flow.getFailReason());
        } else if (flow.getStatus() == ExecutionFlow.Status.DONE) {
            onConsumeSucceed(cheat, flow);
        }

        rapidCheatMapper.modify(cheat);

        unionPayExecutionFlowMapper.updateExecutionFlow(flow, DateUtils.getDateString(cheat.getCreateTime()));

        return cheat.getId();
    }

    public ExecutionFlow pay(String province, String city, QRCode codeModel, BigDecimal amount, User user, String appId, String callbackUrl) {
        if (amount.compareTo(BigDecimal.TEN) < 0) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "交易金额不正确");
        }

        if (!user.getId().equals(codeModel.getUserId())) {
            throw new ServiceException(ResultCode.ERROR_PARAMETER_WRONG, "unsupported");
        }

        var code = matchRegionCode(province, city);
        var merchants = yinshengMapper.getPlaceMerchantInfo(code.getValue1(), code.getValue2());
        if (merchants == null || merchants.size() == 0) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "当前地区不开展业务");
        }

        var consumeCard = cardService.cardWithOwnerCheck(codeModel.getConsumeCardId(), user.getId());


        String userMerchantCode = modifyRemitInfo(merchants.get(0), userService.getCertificate(user.getId()), consumeCard, user);

        var channels = yinshengMapper.pickRandomChannelMerchant(code.getValue1(), code.getValue2());
        String channelCode = channels.get(MathUtils.random(0, channels.size() - 1, false));

        String orderId = HashUtils.getMD5(UUID.randomUUID().toString(), 20);
        var form = ReverseScanForm.builder()
                .authCode(codeModel.getQrCode())
                .channelMercId(channelCode)
                .mercId(userMerchantCode)
                .subject("餐费")
                .totalAmount(amount.doubleValue())
                .orderId(orderId)
                .build();
        var commission = amount.subtract(amount.setScale(2, RoundingMode.FLOOR).multiply(getCommissionRateReversed(user)));
        var flow = ExecutionFlow.builder()
                .type(ExecutionFlow.Type.PAY)
                .amount(amount)
                .cardId(codeModel.getConsumeCardId())
                .commission(commission)
                .orderId(orderId)
                .status(ExecutionFlow.Status.PENDING)
                .vipType(user.getVipType().toString())
                .userId(user.getId())
                .createTime(new Date())
                .build();

        unionPayExecutionFlowMapper.insert(flow, DateUtils.getCurrentDateString());

        if (appId != null) {
            thirdPurchaseFlowMapper.insertFlow(DateUtils.getCurrentDateString(), ThirdPurchaseFlow.builder()
                    .appId(appId)
                    .status(ThirdPurchaseFlow.Status.PENDING)
                    .executionFlowId(flow.getId())
                    .callbackUrl(callbackUrl)
                    .build());
        }

        try {
            request("/api/pay/innerReverseSweep", form, HashMap.class);
            flow.setStatus(ExecutionFlow.Status.DONE);
        } catch (YinshengRequestException e) {
            if (e.yinshengResp != null && "50".equals(e.yinshengResp.getCode())) { // 50也是成功 需要异步查询
                flow.setStatus(ExecutionFlow.Status.PENDING);
            } else {
                String reason = null;
                if (e.yinshengResp != null) {
                    reason = e.yinshengResp.getMsg();
                }

                flow.setFailReason(reason);
            }

            if (e.yinshengResp != null) {
                flow.setExtendInfo(JSONUtil.toJson(e.yinshengResp));
            }
        }

        unionPayExecutionFlowMapper.updateExecutionFlow(flow, DateUtils.getCurrentDateString());
        return flow;
    }


    public ExecutionFlow pay(String province, String city, QRCode codeModel, BigDecimal amount, User user) {
        return pay(province, city, codeModel, amount, user, null, null);
    }

    public RapidCheat queryOrder(String orderId, String userId) {
        RapidCheat cheat = queryOrder(orderId);
        if (!cheat.getUserId().equals(userId)) throw new ServiceException(ResultCode.ERROR_PARAMETER_WRONG, "");
        return query(cheat);
    }

    public RapidCheat queryOrder(String orderId) {
        RapidCheat cheat = rapidCheatMapper.query(orderId);
        return query(cheat);
    }

    @Transactional
    public RapidCheat query(RapidCheat cheat) {
        if (cheat == null) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "流水不存在");
        }
        if (cheat.getStatus() == RapidCheat.Status.REMITTED || cheat.getStatus() == RapidCheat.Status.REMIT_FAILED || cheat.getStatus() == RapidCheat.Status.CONSUME_FAILED) {
            return cheat;
        }

        log.info(JSONUtil.toJson(cheat));

        if (cheat.getStatus() == RapidCheat.Status.CONSUMED && cheat.getRemitFlowId() != null) {
            queryRemitStatus(cheat);
        } else if (cheat.getStatus() == RapidCheat.Status.CREATED && cheat.getConsumeFlowId() != null || cheat.getStatus() == RapidCheat.Status.CONSUMED && cheat.getRemitFlowId() == null) {
            queryConsumeStatus(cheat);
        }

        return cheat;
    }

    @Transactional
    public void queryConsumeStatus(RapidCheat cheat) {
        var flow = unionPayExecutionFlowMapper.getExecutionFlowById(cheat.getConsumeFlowId(), DateUtils.getDateString(cheat.getCreateTime()));
        var result = request("/api/pay/selectOrder", new HashMap<String, String>() {{
            put("orderId", flow.getOrderId());
        }}, OrderQueryResp.class);

        if (result != null) {
            if (result.getResult().equals("00")) {
                onConsumeSucceed(cheat, flow);
                withDraw(cheat);
            } else if (result.getResult().equals("99")) {
                flow.setExtendInfo(JSONUtil.toJson(result));
                onConsumeFailed(cheat, flow, result.getResultMsg());
            }
        }
        rapidCheatMapper.modify(cheat);
        unionPayExecutionFlowMapper.updateExecutionFlow(flow, DateUtils.getDateString(cheat.getCreateTime()));
    }

    @Transactional
    public void queryRemitStatus(RapidCheat cheat) {
        var flow = unionPayExecutionFlowMapper.getExecutionFlowById(cheat.getRemitFlowId(), DateUtils.getDateString(cheat.getCreateTime()));
        var result = request("/api/pay/selectOrder", new HashMap<String, String>() {{
            put("orderId", flow.getOrderId());
        }}, OrderQueryResp.class);

        if (result != null) {
            if (result.getResult().equals("00")) {
                onRemitSucceed(cheat, flow);

            } else if (result.getResult().equals("99")) {
                onRemitFailed(cheat, flow, result.getResultMsg());
            }
        }
        rapidCheatMapper.modify(cheat);
        unionPayExecutionFlowMapper.updateExecutionFlow(flow, DateUtils.getDateString(cheat.getCreateTime()));

    }

    public ExecutionFlow remit(User user, BigDecimal amount, Card remitCard, String appId, String callbackUrl) {
        var code = matchRegionCode(remitCard.getProvince(), remitCard.getCity());
        var merchants = yinshengMapper.getPlaceMerchantInfo(code.getValue1(), code.getValue2());
        var cert = userService.getCertificate(user.getId());
        String userCode = modifyRemitInfo(merchants.get(0), cert, remitCard, user);

        if (userCode == null) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "用户未绑定卡");
        }
        String orderId = HashUtils.getMD5(UUID.randomUUID().toString(), 20);

        var remitFlow = ExecutionFlow.builder()
                .userId(user.getId())
                .status(ExecutionFlow.Status.PENDING)
                .type(ExecutionFlow.Type.REMIT)
                .remitFee(REMIT_FEE)
                .orderId(orderId)
                .amount(amount)
                .createTime(new Date())
                .build();


        unionPayExecutionFlowMapper.insert(remitFlow, DateUtils.getCurrentDateString());

        if (appId != null) {
            thirdPurchaseFlowMapper.insertFlow(DateUtils.getCurrentDateString(), ThirdPurchaseFlow.builder()
                    .appId(appId)
                    .status(ThirdPurchaseFlow.Status.PENDING)
                    .executionFlowId(remitFlow.getId())
                    .callbackUrl(callbackUrl)
                    .build());
        }


        try {
            request("/api/pay/withdrawalOfPay", new HashMap<String, Object>() {{
                put("orderId", orderId);
                put("mercId", userCode);
                put("amount", amount.setScale(2, RoundingMode.FLOOR).toPlainString());
                put("bankAccountNo", remitCard.getNumber());
            }}, Map.class); // 结果不关心 都在外面那层

            onRemitSucceed(null, remitFlow);

            unionPayExecutionFlowMapper.updateExecutionFlow(remitFlow, DateUtils.getDateString(remitFlow.getCreateTime()));
        } catch (YinshengRequestException e) {
            if (e.yinshengResp != null) {
                if ("50".equals(e.yinshengResp.getCode())) {

                } else if ("99".equals(e.yinshengResp.getCode())) {
                    remitFlow.setExtendInfo(JSONUtil.toJson(e.yinshengResp));
                    onRemitFailed(null, remitFlow, e.yinshengResp.getMsg());

                    unionPayExecutionFlowMapper.updateExecutionFlow(remitFlow, DateUtils.getDateString(remitFlow.getCreateTime()));
                }
            }
        }

        return remitFlow;
    }

    @Async
    @Transactional
    public void withDraw(RapidCheat ct) {
        RapidCheat cheat = rapidCheatMapper.query(ct.getId());

        var flow = unionPayExecutionFlowMapper.getExecutionFlowById(cheat.getRemitFlowId(), DateUtils.getDateString(cheat.getCreateTime()));
        var consumeFlow = unionPayExecutionFlowMapper.getExecutionFlowById(cheat.getConsumeFlowId(), DateUtils.getDateString(cheat.getCreateTime()));

        if (flow != null) {
            return; // 幂等, 避免重复提现
        }

        String remitCardId = cheat.getRemitCardId();
        var user = userService.getUser(cheat.getUserId());

        Card remitCard = cardService.cardWithOwnerCheck(remitCardId, user.getId());
        var code = matchRegionCode(remitCard.getProvince(), remitCard.getCity());
        var merchants = yinshengMapper.getPlaceMerchantInfo(code.getValue1(), code.getValue2());
        var cert = userService.getCertificate(user.getId());
        String userCode = modifyRemitInfo(merchants.get(0), cert, remitCard, user);

        if (userCode == null) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "用户未绑定卡");
        }
        String orderId = HashUtils.getMD5(UUID.randomUUID().toString(), 20);

        var remitFlow = ExecutionFlow.builder()
                .userId(cheat.getUserId())
                .status(ExecutionFlow.Status.PENDING)
                .type(ExecutionFlow.Type.REMIT)
                .remitFee(cheat.getRemitFee())
                .orderId(orderId)
                .amount(consumeFlow.getAmount().subtract(consumeFlow.getCommission()).subtract(cheat.getRemitFee()).setScale(2, RoundingMode.FLOOR))
                .build();


        unionPayExecutionFlowMapper.insert(remitFlow, DateUtils.getDateString(cheat.getCreateTime()));
        cheat.setRemitFlowId(remitFlow.getId());
        cheat.setRemitTime(new Date());
        var optimisticCheat = rapidCheatMapper.query(cheat.getId());
        if (optimisticCheat != null && optimisticCheat.getRemitFlowId() != null) {
            return;
        }

        rapidCheatMapper.modify(cheat);

        try {
            request("/api/pay/withdrawalOfPay", new HashMap<String, Object>() {{
                put("orderId", orderId);
                put("mercId", userCode);
                put("amount", remitFlow.getAmount().toPlainString());
                put("bankAccountNo", remitCard.getNumber());
            }}, Map.class); // 结果不关心 都在外面那层

            onRemitSucceed(cheat, remitFlow);

            unionPayExecutionFlowMapper.updateExecutionFlow(remitFlow, DateUtils.getDateString(cheat.getCreateTime()));
            rapidCheatMapper.modify(cheat);
        } catch (YinshengRequestException e) {
            if (e.yinshengResp != null) {
                if ("50".equals(e.yinshengResp.getCode())) {

                } else if ("99".equals(e.yinshengResp.getCode())) {
                    remitFlow.setExtendInfo(JSONUtil.toJson(e.yinshengResp));
                    onRemitFailed(cheat, remitFlow, e.yinshengResp.getMsg());

                    unionPayExecutionFlowMapper.updateExecutionFlow(remitFlow, DateUtils.getDateString(cheat.getCreateTime()));
                    rapidCheatMapper.modify(cheat);
                }
            }
        }


    }

    public void activeWithdrawAllReserveBalance(User user, String remitCardId, BigDecimal amount) {
        Card remitCard = cardService.cardWithOwnerCheck(remitCardId, user.getId());
        var code = matchRegionCode(remitCard.getProvince(), remitCard.getCity());
        var merchants = yinshengMapper.getPlaceMerchantInfo(code.getValue1(), code.getValue2());
        var cert = userService.getCertificate(user.getId());
        String userCode = modifyRemitInfo(merchants.get(0), cert, remitCard, user);
        var balance = amount.setScale(2, RoundingMode.FLOOR);
        String orderId = HashUtils.getMD5(UUID.randomUUID().toString(), 20);

        var remitFlow = ExecutionFlow.builder()
                .userId(user.getId())
                .status(ExecutionFlow.Status.PENDING)
                .type(ExecutionFlow.Type.ACTIVE_REMIT)
                .remitFee(REMIT_FEE)
                .orderId(orderId)
                .amount(balance)
                .createTime(new Date())
                .build();

        unionPayExecutionFlowMapper.insert(remitFlow, DateUtils.getDateString(new Date()));
        try {
            request("/api/pay/withdrawalOfPay", new HashMap<String, Object>() {{
                put("orderId", orderId);
                put("mercId", userCode);
                put("amount", remitFlow.getAmount().toPlainString());
                put("bankAccountNo", remitCard.getNumber());
            }}, Map.class); // 结果不关心 都在外面那层

            onRemitSucceed(null, remitFlow);

            unionPayExecutionFlowMapper.updateExecutionFlow(remitFlow, DateUtils.getDateString(remitFlow.getCreateTime()));
        } catch (YinshengRequestException e) {
            if (e.yinshengResp != null) {
                if ("50".equals(e.yinshengResp.getCode())) {

                } else if ("99".equals(e.yinshengResp.getCode())) {
                    remitFlow.setExtendInfo(JSONUtil.toJson(e.yinshengResp));
                    onRemitFailed(null, remitFlow, e.yinshengResp.getMsg());

                    unionPayExecutionFlowMapper.updateExecutionFlow(remitFlow, DateUtils.getDateString(remitFlow.getCreateTime()));
                }
            }
        }
    }

    public void queryFlow(String flowId, String dateString) {
        var flow = unionPayExecutionFlowMapper.getExecutionFlowById(flowId, dateString);
        if (flow.getStatus() == ExecutionFlow.Status.PENDING) {
            if (flow.getType() != ExecutionFlow.Type.ACTIVE_REMIT) {
                String columnName = flow.getType() == ExecutionFlow.Type.PAY ? "consumeFlowId" : "remitFlowId";

                RapidCheat cheat = rapidCheatMapper.queryByExecutionFlow(columnName, flow.getId());
                if (cheat != null) return; // 只处理和RapidCheat无关的流水的状态流转, 这里银联交易需要重构一下
            }

            var result = request("/api/pay/selectOrder", new HashMap<String, String>() {{
                put("orderId", flow.getOrderId());
            }}, OrderQueryResp.class);

            if (result != null) {
                boolean shouldCallback = true;
                if (result.getResult().equals("00")) {
                    if (flow.getType() == ExecutionFlow.Type.PAY) {
                        onConsumeSucceed(null, flow);
                    } else {
                        onRemitSucceed(null, flow);
                    }

                } else if (result.getResult().equals("99")) {
                    if (flow.getType() == ExecutionFlow.Type.PAY) {
                        onConsumeFailed(null, flow, result.getResultMsg());
                    } else {
                        onRemitFailed(null, flow, result.getResultMsg());
                    }
                } else {
                    shouldCallback = false;
                }
                unionPayExecutionFlowMapper.updateExecutionFlow(flow, dateString);

                if (shouldCallback) {
                    try {
                        var thirdFlow = thirdPurchaseFlowMapper.flowByExecutionFlowId(DateUtils.getDateString(flow.getCreateTime()), flow.getId());
                        tradeService.callbackExecutionFlow(flow, thirdFlow);
                    } catch (BadSqlGrammarException e) {
                        log.error("flow not found:", e);
                    }
                }

            }

        }

    }

    public List<ExecutionFlow> getPendingFlowsOfDateString(String dateString) {
        try {
            return unionPayExecutionFlowMapper.getPendingFlowsOfDateString(dateString);
        } catch (Throwable ignored) {
            // 有异常说明表不存在, 忽略
            return new ArrayList<>();
        }
    }

    /**
     * @return userCode
     */
    private String modifyRemitInfo(Merchant merchant, Certificate certificate, Card card, User user) {

        if (certificate == null) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "请先实名认证");
        }

        String userCode = yinshengMapper.userCodeByUserId(certificate.getUserId());

        merchant.setUserCode(userCode);
        merchant.setContactsTel(card.getPhoneNumber());

        Tuple<String, String> codes;

        if (card.getType() == Card.Type.CREDIT) { // 信用卡没有归属地信息, 直接用北京来进件
            codes = new Tuple<>("1001", "1000");
        } else {
            codes = matchRegionCode(card.getProvince(), card.getCity());
        }

        merchant.setBankProvince(codes.getValue1());
        merchant.setBankCity(codes.getValue2());
        merchant.setBankAccountNo(card.getNumber());

        String bankCode = configFileService.getYinShengBankType().get(card.getBankName());
        if (bankCode == null) {
            bankCode = "9999999";
        }
        merchant.setBanktype(bankCode);
        merchant.setImageBase64Str(ossServiceImpl.signUrl(certificate.getPhotoUrl()));
        merchant.setBankCardType("11");
        merchant.setFeeRat1(getCommissionRateYinshengFormat(user));
        merchant.setFeeRat2(getCommissionRateYinshengFormat(user));
        merchant.setWithdrawFee(0.);
        merchant.setFixFeeAmt(REMIT_FEE.doubleValue());

        merchant.setCrpNm(certificate.getName());
        merchant.setCrpIdNo(certificate.getIdCode());
        merchant.setIdAddress(certificate.getAddress());


        try {
            Map result = request("/api/MicroMerchant/bind", merchant, HashMap.class);
            if (userCode == null) {
                if (result != null && result.containsKey("userCode")) {
                    userCode = (String) result.get("userCode");
                    yinshengMapper.addUserCodeBound(certificate.getUserId(), userCode);
                } else {
                    throw new ServiceException(ResultCode.ERROR_PARAMETER_WRONG, "yinsheng bound merchant return empty userCode");
                }
            }

            return userCode;
        } catch (YinshengRequestException e) {
            if (e.yinshengResp != null && "90".equals(e.yinshengResp.getCode())) {
                yinshengMapper.addUserCodeBound(certificate.getUserId(), queryUserCode(certificate.getIdCode()));
                return modifyRemitInfo(merchant, certificate, card, user);
            }

            throw e;
        }
    }

    public String queryUserCode(String idNo) {
        return request("/api/MicroMerchant/query", new HashMap<String, String>() {{
            put("regId", idNo);
        }}, HashMap.class).get("userCode").toString();
    }

    public SimpleVerifyCodeTokenVo bindCard(String cardId, User user) {
        Card card = cardService.cardWithOwnerCheck(cardId, user.getId());
        Certificate certificate = userService.getCertificate(user.getId());
        String yinshengUserId = yinshengMapper.getYinshengUserId(user.getId());
        String userAppCode = HashUtils.getMD5(user.getId(), 20);

        String userCode = yinshengMapper.userCodeByUserId(certificate.getUserId());
        if (userCode == null) {
            // 没进件绑卡会失败, 先强行进件, 避免绑卡失败
            modifyRemitInfo(yinshengMapper.getPlaceMerchantInfo("1001", "1000").get(0), certificate, card, user);
        }


        if (configFileService.getYinShengBlackBankList().contains(card.getBankName())) {
            throw new ServiceException(ResultCode.ERROR_UNION_CARD_UNSUPPORTED_ERR, "未开通此银行业务");
        }

        BindCardForm form = BindCardForm.builder()
                .cardNo(card.getNumber())
                .mobile(card.getPhoneNumber())
                .name(certificate.getName())
                .idCard(certificate.getIdCode())
                .appUserCode(userAppCode)
                .gps(Constants.getCurrentGPSOrError())
                .deviceType(Constants.getCurrentDeviceType())
                .deviceId(Constants.getCurrentDeviceId())
                .build();

        if (yinshengUserId == null) {
            form.setUserId(form.getAppUserCode());
            form.setFuncode("open_account");
        } else {
            form.setUserId(yinshengUserId);
            form.setFuncode("bind_card");
        }


        try {
            BindCardResp resp = request("/user/sendSms", form, BindCardResp.class);
            resp.setCardId(cardId);
            resp.setUserAppCode(userAppCode);


            String token = UUID.randomUUID().toString();
            redis.boundValueOps(PREFIX + token).set(JSONUtil.toJson(resp), 24 * 60 * 60, TimeUnit.SECONDS);

            return SimpleVerifyCodeTokenVo.builder().number(card.getPhoneNumber()).token(token).build();
        } catch (YinshengRequestException e) {
            if (e.yinshengResp != null && "90".equals(e.yinshengResp.getCode())) {
                throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "信息校验不通过");
            } else {
                throw e;
            }
        }

    }

    public Card verifyBindCard(String token, String verifyCode, User user) {
        String json = redis.boundValueOps(PREFIX + token).get();
        if (json == null) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "验证码不存在, 请重试");
        }
        BindCardResp tokenModel = JSONUtil.parseJson(json, BindCardResp.class);
        var card = cardService.cardWithOwnerCheck(tokenModel.getCardId(), user.getId());
        Certificate certificate = userService.getCertificate(user.getId());
        String yinshengUserId = yinshengMapper.getYinshengUserId(user.getId());
        String userAppCode = HashUtils.getMD5(user.getId(), 20);

        BindCardForm form = BindCardForm.builder()
                .cardNo(card.getNumber())
                .mobile(card.getPhoneNumber())
                .name(certificate.getName())
                .idCard(certificate.getIdCode())
                .appUserCode(userAppCode)
                .gps(Constants.getCurrentGPSOrError())
                .deviceType(Constants.getCurrentDeviceTypeOrError())
                .deviceId(Constants.getCurrentDeviceIdOrError())

                .cardValidity(StringUtils.ifNull(card.getValidation(), ""))
                .securityCode(StringUtils.ifNull(card.getCvv2(), ""))
                .checkId(tokenModel.getCheckId())
                .verificationCode(verifyCode)
                .verificationCodeId(tokenModel.getVerificationCodeId())
                .build();

        if (yinshengUserId == null) {
            form.setType("00");
        } else {
            form.setType("01");
        }

        try {
            var resp = request("/user/bindCard", form, BindCardResp.class);

            yinshengMapper.insertYinshengBound(card.getId(), user.getId(), yinshengUserId == null ? resp.getUserId() : yinshengUserId);
            return card;
        } catch (YinshengRequestException e) {
            if (e.yinshengResp != null) {
                if ("90".equals(e.yinshengResp.getCode())) {
                    throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "信息校验不通过");
                } else if ("99".equals(e.yinshengResp.getCode()) && "重复绑卡".equals(e.yinshengResp.getMsg()) && yinshengUserId != null) {
                    yinshengMapper.insertYinshengBound(card.getId(), user.getId(), yinshengUserId);
                    return card;
                }


            }

            throw e;
        }

    }

    public boolean checkCardBounded(String cardId, String userId) {


        var card = cardService.cardWithOwnerCheck(cardId, userId);
        if (configFileService.getYinShengBlackBankList().contains(card.getBankName())) {
            throw new ServiceException(ResultCode.ERROR_UNION_CARD_UNSUPPORTED_ERR, "未开通此银行业务");
        }
        return yinshengMapper.checkCardBound(userId, cardId) != null;
    }

    public BigDecimal queryBalance(User user) {
        String userCode = yinshengMapper.userCodeByUserId(user.getId());
        if (userCode == null) {
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "用户未绑定卡");
        }
        var resp = request("/api/pay/selectWithdrawal", new HashMap<String, String>() {{
            put("mercId", userCode);
        }}, BalanceResp.class, BalanceResp.class);

        if ("00".equals(resp.getCode())) {
            return BigDecimal.valueOf(resp.getData()).setScale(5, RoundingMode.FLOOR);
        } else {
            throw new YinshengRequestException(ResultCode.ERROR_TOAST_ERR, resp.getMsg(), null);
        }
    }

    public void clearUserBalance(User user) {
        try {

            var balance = queryBalance(user);
            if (balance.compareTo(new BigDecimal("20")) >= 0) {
                var remitCard = cardService.userDefaultCard(user);
                if (remitCard == null) {
                    var cards = cardService.getUserCard(user.getId());
                    for (var c : cards) {
                        if (c.getType() == Card.Type.DEBIT) {
                            remitCard = c;
                            break;
                        }
                    }
                }

                if (remitCard != null) {
                        activeWithdrawAllReserveBalance(user, remitCard.getId(), balance);


                }

            }
        } catch (Throwable t) {
            log.error("ignored", t);
        }
    }

    private double getCommissionRateYinshengFormat(User user) {
        var reversed = getCommissionRateReversed(user);
        return BigDecimal.ONE.subtract(reversed).multiply(BigDecimal.TEN).multiply(BigDecimal.TEN).doubleValue();
    }

    private BigDecimal getCommissionRateReversed(User user) {
        // 提现费率稳定0.0001, 要反算整体费率
        switch (user.getVipType()) {
            case PAYED:
                return BigDecimal.valueOf(0.9962); //0.38%
            case PRIME:
                return BigDecimal.valueOf(0.9966); //0.34%
            default:
            case ORDINARY:
                return BigDecimal.valueOf(0.9951); //0.49%
        }
    }


    private <T> T request(String url, Object params, Class<T> resultType) {
        return request(url, params, resultType, null);
    }

    private <T> T request(String url, Object params, Class<T> resultType, Class<T> wrapperClazz) {
        Map<String, Object> paramMap = JSONUtil.objectToMap(params);
        paramMap.put("ip", Constants.getCurrentRemoteIp());
        paramMap.put("accessCode", accessCode);
        paramMap.put("out_timestamp", ((Long) new Date().getTime()).toString());

        paramMap.put("out_sign", sign(paramMap, appKey).toLowerCase());


        String reqJson = JSONUtil.toJson(paramMap);

        log.info("request to:" + url + " body: " + reqJson);

        Request r = new Request.Builder()
                .url(apiBaseUrl + url)
                .post(RequestBody.create(reqJson, MediaType.parse("application/json;charset-utf8")))
                .header("Content-Type", "application/json;charset-utf8")
                .build();

        try {
            var call = ClientHolder.getClient().newCall(r);
            Response resp = call.execute();

            String respString = resp.body().string();
            log.info("request to:" + url + " resp: " + respString);

            if (wrapperClazz != null) {
                return JSONUtil.parseJson(respString, wrapperClazz);
            }

            CommonResp respObj = JSONUtil.parseJson(respString, CommonResp.class);
            if ("00".equals(respObj.getCode())) {
                return respObj.getData() != null ? JSONUtil.parseJson(respObj.getData(), resultType) : null;
            } else {
                log.error("request:" + url + " err, code:" + respObj.getCode() + ", msg:" + respObj.getMsg());
                if (respObj.getMsg().contains("[6158087]")) {
                    throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "验证码已过期");
                }

                if (respObj.getMsg().equals("请勿频繁操作")) {
                    throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "触发交易频控");
                }

                throw new YinshengRequestException(ResultCode.ERROR_INTERNAL_DATA_CODE, respObj.getMsg(), respObj);
            }
        } catch (ServiceException e) {
            throw e;
        } catch (Throwable t) {
            throw new ServiceException(ResultCode.ERROR_PARAMETER_WRONG, "", t);
        }
    }


    private static String sign(Map<String, Object> params, String appKey) {
        StringBuilder valueSb = new StringBuilder();   // 将参数以参数名的字典升序排序
        Map<String, Object> sortParams = new TreeMap<String, Object>(params);
        Set<Map.Entry<String, Object>> entries = sortParams.entrySet();

        for (Map.Entry<String, Object> entry : entries) {
            valueSb.append(entry.getKey() + "=" + entry.getValue() + "&");
        }
//        把私钥拼接在数据后面
        String signature = valueSb.toString() + "appKey=" + appKey;
        return HashUtils.getMD5(signature);
    }

    private Tuple<String, String> matchRegionCode(String prov, String city) {
        var collections = configFileService.getYinShengAddressRelationShip().getProvinces();
        GeoInfo provInfo = null;
        for (var province : collections) {
            if (province.getName().equals(prov)) {
                provInfo = province;

                break;
            }
        }

        if (provInfo == null) {
            log.warn("matching code of " + prov + " " + city + " err");
            throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "所在地区不开展业务");
        }

        for (var cityObj : provInfo.getCites()) {
            if (cityObj.getName().equals(city)) {
                return new Tuple<>(provInfo.getCode(), cityObj.getCode());
            }
        }

        log.warn("matching code of " + prov + " " + city + " err");
        throw new ServiceException(ResultCode.ERROR_TOAST_ERR, "所在地区不开展业务");
    }

    private class YinshengRequestException extends ServiceException {
        private CommonResp yinshengResp;

        public YinshengRequestException(Long code, String message, CommonResp yinshengResp) {
            super(code, message);
            this.yinshengResp = yinshengResp;
        }
    }

    private void onConsumeSucceed(RapidCheat cheat, ExecutionFlow flow) {

        flow.setStatus(ExecutionFlow.Status.DONE);
        if (cheat != null) {
            cheat.setConsumeDoneTime(new Date());
            cheat.setStatus(RapidCheat.Status.CONSUMED);
            withDraw(cheat);
        }

        userService.setUserActive(flow.getUserId()); // 支付成功设为活跃用户
    }

    private DecimalFormat df = new DecimalFormat("#,##0.00");

    private void onConsumeFailed(RapidCheat cheat, ExecutionFlow flow, String reason) {
        if (cheat != null) {
            cheat.setConsumeDoneTime(new Date());
            cheat.setStatus(RapidCheat.Status.CONSUME_FAILED);

            messageService.addMessage(Message.builder()
                    .userId(flow.getUserId())
                    .type(Message.Type.SYSTEM)
                    .title("[银联闪付]交易失败")
                    .content("您刚刚提交的" + df.format(flow.getAmount()) + "元交易失败, 如有疑问请{{联系客服}}处理")
                    .build(), true);
        }

        flow.setStatus(ExecutionFlow.Status.FAILED);
        flow.setFailReason(reason);



    }

    private void onRemitSucceed(RapidCheat cheat, ExecutionFlow flow) {
        flow.setStatus(ExecutionFlow.Status.DONE);
        if (cheat != null) {
            cheat.setStatus(RapidCheat.Status.REMITTED);
            cheat.setRemitDoneTime(new Date());
            messageService.addMessage(Message.builder()
                    .userId(cheat.getUserId())
                    .type(Message.Type.SYSTEM)
                    .title("[闪付扫码]收款成功")
                    .content("您刚刚交易了" + df.format(cheat.getAmount()) + "元, 到账金额为" + df.format(flow.getAmount()) + ", 如有疑问请{{联系客服}}")
                    .build(), true);
        }
    }

    private void onRemitFailed(RapidCheat cheat, ExecutionFlow flow, String failReason) {
        flow.setStatus(ExecutionFlow.Status.FAILED);
        flow.setFailReason(failReason);

        if (cheat != null) {
            cheat.setStatus(RapidCheat.Status.REMIT_FAILED);
            cheat.setRemitDoneTime(new Date());

            messageService.addMessage(Message.builder()
                    .userId(cheat.getUserId())
                    .type(Message.Type.SYSTEM)
                    .title("[闪付扫码]收款失败")
                    .content("您刚刚交易了" + df.format(cheat.getAmount()) + "元并支付成功. 应到账金额" + df.format(flow.getAmount()) + "元提现失败, 请{{联系客服}}处理")
                    .build(), true);
        }

    }


}
